[C 기술면접] 전처리기 define 매크로 — 우선순위·부작용·do-while(0) 함정

NOTE

질문 10: #define을 쓸 때 구문(문장)·변수·표현식을 넣을 경우 주의할 점은? (우선순위, do { } while(0) 등) 핵심 한 문장: 매크로는 “값/타입을 가진 함수”가 아니라 컴파일 전에 일어나는 단순 “텍스트 치환”이다. 그래서 연산자 우선순위, 인자 다중 평가(부작용), 문장 경계(세미콜론·if-else), 이름 충돌(스코프)이 모두 문제가 된다. 전개 결과를 머릿속(또는 gcc -E)으로 항상 확인한다.


1. 매크로의 본질

  • 전처리기가 토큰 단위로 치환한다. 타입 검사 없음, 스코프 없음, 디버거에서 보이지 않음(이름이 사라짐).
  • 인자는 평가된 값이 아니라 표현식 텍스트가 그대로 들어간다 → 인자가 여러 번 나오면 여러 번 평가된다.
  • 전개를 확인하는 습관: gcc -E file.c | less (또는 -save-temps).

2. 체크리스트 — 매크로를 쓸 때 주의할 점

#주의점증상해결
1연산자 우선순위 — 인자 괄호SQUARE(1+2)가 1+2*1+2 = 5매크로 본문에서 각 인자를 (x) 로 감싼다
2연산자 우선순위 — 전체 괄호DOUBLE(3)*2가 3+3*2 = 9전체 표현식을 ( ... ) 로 감싼다
3인자 다중 평가(부작용)MAX(i++, j)에서 i가 두 번 증가부작용 있는 인자를 넘기지 않는다 / 함수·static inline 사용 / GNU typeof+문장식
4여러 문장 + if/else두 번째 문장이 if 밖으로 빠지고 else가 짝을 잃음do { ... } while (0)
5세미콜론 처리매크로 정의 끝에 ;를 넣으면 호출부 ;와 겹쳐 빈 문장 → if/else 깨짐정의 끝에는 ; 넣지 않는다, 호출하는 쪽이 붙인다
6줄 연속 \\ 뒤에 공백이 있으면 치환 실패/경고\는 줄 맨 끝에, 뒤에 공백 금지
7매크로 안 지역 변수 이름 충돌호출부 변수 tmp와 매크로 내부 tmp가 섀도잉고유한 이름(_tmp_는 예약 영역 주의, tmp__mac 등) 또는 함수로 대체
8return/goto/break가 매크로에 숨음호출부에서 흐름 제어가 안 보임이름에 드러내거나(CHECK_NULL_RETURN) 사용을 최소화
9타입 안전성 없음서로 다른 타입 비교, 정수 승격 문제static inline 함수, enum, const
10인자에 쉼표MAX(f(a,b), c)에서 쉼표가 인자 구분자로 해석됨(괄호 안은 OK)괄호로 감싸거나 가변 인자(__VA_ARGS__) 활용
11#(문자열화) / ##(토큰 결합)인자가 매크로면 전개되기 전에 문자열화/결합됨한 단계 더 거치는 간접 매크로로 우회
12sizeof 배열 크기 매크로포인터에 쓰면 포인터 크기 나옴(decay)배열 인자에만 사용, 가능하면 컴파일 타임 검사 추가
13매크로 이름 충돌/오염짧은 이름(MAX, MIN, ERROR)이 라이브러리와 충돌모듈 접두어, 필요 시 #undef, 헤더에 부작용 큰 매크로 정의 지양
14헤더 가드 / 재정의같은 매크로 두 번 정의 → 경고/에러#ifndef 가드, #undef 후 재정의
15예약 식별자__FOO__, _Foo 정의는 표준상 예약밑줄로 시작하지 않는 이름 사용

3. 핵심 사례

3.1 우선순위 — 괄호는 “인자마다, 전체에”

#define SQUARE(x)   x * x
SQUARE(1 + 2)          /* 1 + 2 * 1 + 2 = 5   (기대 9) */
 
#define SQUARE_OK(x)   ((x) * (x))
SQUARE_OK(1 + 2)       /* ((1 + 2) * (1 + 2)) = 9 */
 
#define DOUBLE(x)  (x) + (x)         /* 전체 괄호 누락 */
DOUBLE(3) * 2          /* (3) + (3) * 2 = 9  (기대 12) */
 
#define DOUBLE_OK(x) ((x) + (x))
DOUBLE_OK(3) * 2       /* 12 */
 
#define IS_ODD(x)  x % 2 == 1        /* 우선순위 + 인자 괄호 모두 문제 */
#define IS_ODD_OK(x)  (((x) % 2) == 1)
  • -1 % 2는 C99부터 -1이라 IS_ODD가 음수에서 틀릴 수 있으므로 != 0으로 쓰는 편이 안전하다. ((((x) % 2) != 0))

3.2 부작용 — 인자가 여러 번 평가된다

#define MAX(a, b)  ((a) > (b) ? (a) : (b))
int i = 5, j = 3;
int m = MAX(i++, j);   /* ((i++) > (j) ? (i++) : (j)) → i++ 가 두 번 실행될 수 있음 → UB 가능 */

해결:

/* 1) 가장 좋은 방법: 함수 */
static inline int max_int(int a, int b) { return a > b ? a : b; }
 
/* 2) GNU 확장: 한 번만 평가 + 타입 유지 */
#define MAX_SAFE(a, b) ({ __typeof__(a) _a = (a); __typeof__(b) _b = (b); _a > _b ? _a : _b; })
 
/* 3) C11 _Generic 으로 타입별 inline 함수 선택 */
  • 매크로 인자에 ++, --, 함수 호출(부작용·비용), rand(), getc()를 넘기지 않는다.

3.3 do { ... } while (0) — 여러 문장을 문장 하나로

#define FREE_AND_NULL(p)  free(p); p = NULL;        /* 나쁨 */
 
if (cond)
    FREE_AND_NULL(ptr);        /* → if (cond) free(ptr); ptr = NULL;;  ptr = NULL 은 항상 실행 */
else                            /* else 가 짝을 잃어 컴파일 에러 */
    other();

{ }로만 감싸면:

#define FREE_AND_NULL(p)  { free(p); (p) = NULL; }
if (cond) FREE_AND_NULL(ptr);   /* → if (cond) { ... } ;  ← 뒤의 ';' 가 빈 문장이 되어 else 와 끊김 */
else other();

정답:

#define FREE_AND_NULL(p) \
    do {                 \
        free(p);         \
        (p) = NULL;      \
    } while (0)               /* 여기에는 세미콜론을 붙이지 않는다 */
 
if (cond) FREE_AND_NULL(ptr);  /* 호출부의 ';' 가 do-while 의 종결자가 됨 → 안전 */
else      other();
  • do-while(0)은 문법적으로 문장 하나이면서 내부에 지역 변수를 선언하고 break로 조기 탈출도 할 수 있다. 컴파일러가 조건 검사를 최적화로 제거해 비용이 없다.
  • 한계: 문장이라 값을 반환할 수 없다. 값이 필요하면 static inline 함수, 콤마 연산자 표현식, GNU 문장식 ({ ... }).
  • 매크로 안에서 return을 쓰는 방어 매크로도 이 패턴으로:
#define CHECK_NULL_RETURN(p, ret) \
    do { if ((p) == NULL) { LOG_ERR("null: %s\n", #p); return (ret); } } while (0)

→ 참고: 매크로를 do-while(0)으로 감싸는 이유, 전처리기 문법 총정리

3.4 #, ##, 가변 인자 매크로

#define STR(x)        #x                 /* 문자열화:  STR(abc) → "abc" */
#define CAT(a, b)     a##b               /* 토큰 결합: CAT(x, 1) → x1 */
 
/* 인자가 다른 매크로면 전개 전에 문자열화되는 문제 → 한 단계 간접 */
#define XSTR(x)       STR(x)
#define VER 3
STR(VER)    /* "VER" */
XSTR(VER)   /* "3"   */
 
/* 가변 인자 로그 매크로 */
#define LOG(fmt, ...)  fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
/* ##__VA_ARGS__ 는 GNU 확장: 가변 인자가 없을 때 앞의 쉼표를 제거. C23 에서는 __VA_OPT__ */
LOG("start");
LOG("value=%d", v);
  • 이름 규칙: 로그 매크로 안에서 #p를 쓰면 인자 텍스트를 문자열로 찍을 수 있어 디버깅에 유용하다(#ptr → "ptr").

3.5 매크로 vs const / enum / inline

용도매크로더 나은 대안
상수#define MAX_SIZE 100enum { MAX_SIZE = 100 }; 또는 static const int (디버거에서 이름이 보이고 타입·스코프 있음)
정수 상수 집합여러 #defineenum
짧은 함수#define SQ(x) ...static inline 함수 (타입 검사, 단일 평가)
조건부 컴파일#ifdef불가피하게 매크로가 필요한 영역
문자열화/결합, __FILE__/__LINE__매크로 필요—
헤더 가드#ifndef#pragma once (비표준이나 널리 지원)
  • 매크로가 필요한 경우: 조건부 컴파일, 헤더 가드, __FILE__/__LINE__ 삽입, 문자열화/토큰 결합, 제어 흐름(return 포함 방어 매크로), 타입 독립 코드(제네릭 컨테이너).

4. 꼬리 질문

질문답변 요지
매크로와 인라인 함수의 차이?매크로는 전처리 단계의 텍스트 치환(타입 검사·단일 평가 없음), inline은 컴파일러가 처리하는 함수(타입 안전, 인자 한 번 평가, 디버깅 가능). 대부분 static inline이 낫다.
#define과 const의 차이?#define은 전처리 치환(메모리 없음, 타입·스코프 없음), const는 타입과 스코프가 있는 변수(주소 취득 가능, 디버거에서 보임). C에서 배열 크기 상수로는 const가 아닌 enum/#define이 필요한 경우가 있다(C99 VLA 제외).
do-while(0) 말고 { } 만 쓰면 안 되나?if (c) M(); else ...에서 {...}; 뒤 세미콜론이 빈 문장이 되어 else가 끊긴다.
매크로 안에서 break/continue/return을 쓰면?호출한 함수/루프의 흐름이 바뀐다. do-while(0) 안의 break는 do-while만 탈출하므로 의도와 다를 수 있다. 의도를 이름에 드러낸다.
#if와 #ifdef의 차이?#ifdef X는 정의 여부만, #if X는 값을 평가(미정의 식별자는 0). #if defined(A) && B > 2처럼 조합 가능.
매크로가 디버깅이 어려운 이유?전처리 후에는 이름이 사라져 디버거가 못 보고, 컴파일 에러가 전개된 코드 기준으로 나온다. gcc -E로 전개 확인, -g3로 매크로 정보를 디버그 정보에 포함 가능.
다중 포함으로 인한 문제?헤더 가드(#ifndef) 필수, 가드 이름은 밑줄로 시작하지 않는다.
ARRAY_SIZE 매크로의 함정?#define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0]))는 배열에만 유효. 함수 인자로 받은 포인터에 쓰면 sizeof(ptr)/sizeof(*ptr)가 되어 틀린 값. (리눅스는 __must_be_array 검사 추가)
volatile/static 같은 키워드를 매크로로 감싸도 되나?가능은 하지만 가독성을 해친다. 팀 컨벤션이 아니면 지양.

한 줄 요약

  • 인자마다 괄호, 전체에 괄호(우선순위) · 부작용 있는 인자 금지(다중 평가) · 여러 문장은 do { } while (0), 정의 끝 세미콜론 금지 · 가능하면 static inline / enum / const로 대체 · 애매하면 gcc -E로 전개를 확인한다.

관련 문서